Skip to content

Add ALL and EXACTLY_ONE array filter operators for MongoDB and Postgres - #323

Open
lrathod wants to merge 12 commits into
hypertrace:mainfrom
lrathod:ASP-3008/array-match-all-one-operators
Open

Add ALL and EXACTLY_ONE array filter operators for MongoDB and Postgres#323
lrathod wants to merge 12 commits into
hypertrace:mainfrom
lrathod:ASP-3008/array-match-all-one-operators

Conversation

@lrathod

@lrathod lrathod commented Aug 27, 2026

Copy link
Copy Markdown

Summary

Adds two new ArrayOperator values for filtering on array-valued attributes:

  • ALL — array attribute must contain every value specified in the filter. Set-containment semantics: order and duplicates are irrelevant on both sides ([red, red] ALL [red] is true).
  • EXACTLY_ONE — array attribute must contain exactly one element, and that element must be one of the specified values. The cardinality check counts raw elements, not distinct values ([red, red] EXACTLY_ONE [red] is false).

Both work on top-level and nested array fields (e.g. props.colors, scope.environmentScope.environmentIds), on MongoDB and Postgres.

MongoDB

  • ALL{"$expr": {"$setIsSubset": [<values>, <guarded array>]}}
  • EXACTLY_ONE$and of $size == 1 and $in on $arrayElemAt [path, 0]
  • Non-array guard: <guarded array> is {"$cond": [{"$isArray": "$path"}, "$path", []]} — documents holding a missing/null/non-array scalar value simply do not match instead of erroring ($setIsSubset/$size reject non-array operands), matching the Postgres behaviour.

Postgres

  • Native array columns (flat collections): ALLcol @> ?; EXACTLY_ONEarray_length(col, 1) = 1 AND col && ?. No COALESCE — NULL arrays are excluded by WHERE semantics anyway, and the unwrapped column reference keeps the filter GIN-indexable (SARGable).
  • JSONB array paths (nested documents): ALL(CASE WHEN jsonb_typeof(path) = 'array' THEN path ELSE '[]'::jsonb END) @> ?::jsonb; EXACTLY_ONEjsonb_array_length(<guarded>) = 1 AND <guarded> <@ ?::jsonb (single bound param — with exactly one element, membership ≡ containment). The runtime jsonb_typeof guard is retained only for schemaless JSONB paths.
  • Compile-time element types: the array element type is resolved from the field expression's DataType (ArrayIdentifierExpression#getElementDataType()), falling back to inference from the filter values only when the field carries no type info.

Design note

Both parsers require the inner RelationalExpression to carry a constant value list; a non-constant RHS throws UnsupportedOperationException. This is intentional — ALL/EXACTLY_ONE are set-level operators, unlike ANY which supports arbitrary per-element sub-filters. Empty value lists are rejected at construction by ConstantExpression.

Future consideration (noted in the enum javadoc): an EXACTLY operator for set equality — array contains exactly the filter values, no more and no less.

Test coverage / EXACTLY_ONE semantics

MATCH_EXACTLY_ONE describes the stored array cardinality, not the length of the RHS:

  • Stored array must have exactly 1 element
  • That element must be in the RHS list (RHS can be 1 or many candidates)

Example with column tags:

  • Entity A: ["A", "B"]
  • Entity B: ["A"]
  • Entity C: ["B"]
  • Entity D: ["A", "B", "C"]
Query Result
MATCH_ALL ["A", "B"] A and D (D matches because extras are allowed; ALL is subset / containment)
MATCH_EXACTLY_ONE ["A"] B only
MATCH_EXACTLY_ONE ["A", "B"] B and C (not A, not D — size ≠ 1)

Same translation on both stores: Mongo $size=1 + $in; Postgres native array_length=1 AND && / JSONB length + <@.

Test plan

  • MongoArrayFilterParserTest — operator structure, $isArray guards, nested paths, non-constant RHS rejection, no double $expr wrapping
  • PostgresQueryParserTest — ALL/EXACTLY_ONE × JSONB/native array, nested JSONB paths, compile-time type precedence over value inference, UNSPECIFIED fallback, non-constant RHS rejection
  • DocStoreQueryV1Test (nested ArrayMatchAllOneOperatorTest) — integration tests on both datastores: nested JSONB array fields, native array columns (typed + untyped via PostgresArrayTypeProvider), JSONB array column on flat collections, 3-level nested paths with missing intermediate objects, non-array scalar values, duplicates ([red, red]), and order-independence — run in CI
  • :document-store:build (compile + unit tests + spotless) passes locally

Made with Cursor

Nested arrays

  • Nested arrays: operators apply to the outermost array only — an element that is itself an array is opaque and never matches a scalar RHS value; RHS lists are scalar-only. Pinned by IT testAllAndOneTreatNestedArrayElementsAsOpaque.

Add two new ArrayOperator values for filtering on array-valued attributes:
- ALL: array attribute must contain every value specified in the filter
- ONE: array attribute must contain exactly one element, and that element
  must be one of the specified values

MongoDB: ALL uses $setIsSubset with an $ifNull guard; ONE combines
$size == 1 with $in on the first element via $arrayElemAt.

Postgres: native array columns use @> (ALL) and array_length + && (ONE);
JSONB array paths use jsonb_typeof-guarded @> containment and
jsonb_array_length respectively.

Both parsers require the inner filter to carry a constant value list;
non-constant RHS expressions throw UnsupportedOperationException since
these are set-level operators, not per-element predicates like ANY.

Co-authored-by: Cursor <cursoragent@cursor.com>
return value instanceof List ? (List<?>) value : List.of(value);
}

private PostgresDataType resolvePostgresDataType(final List<?> values) {

@suddendust suddendust Aug 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid a runtime check? ArrayIdentifierExpression contains DataType that can be extracted statically.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The native-array branch now resolves the element type from the compile-time type info on the field expression (ArrayIdentifierExpression#getElementDataType() / IdentifierExpression#getDataType()) via PostgresDataType.fromDataType, and only falls back to inferring from the filter values when the field carries no type info (UNSPECIFIED). The runtime jsonb_typeof guard is now retained only for the JSONB/nested-array path, where the value is schemaless and can be JSON null or a non-array at runtime. Added unit tests covering both the compile-time precedence (declared long[] wins over Integer values) and the fallback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, let me update,
for nested arrays, we'd still need runtime check

@suddendust

Copy link
Copy Markdown
Contributor

What about operators for nested json arrays?

@suddendust

Copy link
Copy Markdown
Contributor

Lets add some integration tests? You can add in DocStoreQueryV1Test.

*/
@ParameterizedTest
@ArgumentsSource(AllProvider.class)
void getDocumentsContainingAllGivenValues(final String dataStoreName)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see we already have ITs. Can we move them to DocStoreQueryV1Test?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, let me move

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Moved all ALL/ONE integration tests into DocStoreQueryV1Test as a nested ArrayMatchAllOneOperatorTest class and removed them from ArrayFiltersQueryIntegrationTest. They now run against the shared collections: nested JSONB path (props.colors) on both stores, native array columns (tags/flags) on the flat collection using the existing PostgresArrayTypeProvider (typed + untyped variants), and the JSONB array column on the flat collection.

lrathod and others added 2 commits September 1, 2026 11:16
…mantics

- Resolve native array element type from the compile-time type info on the
  field expression (ArrayIdentifierExpression/IdentifierExpression DataType)
  instead of inferring from filter values; value inference is now only a
  fallback when no type info is present. The runtime jsonb_typeof guard is
  retained only for schemaless JSONB/nested array paths.
- Add unit + integration tests covering ALL/ONE on nested array fields
  (e.g. props.colors, scope.environmentScope.environmentIds) for both
  MongoDB and Postgres.
- Add integration test documenting that ALL is set-containment: duplicates
  in the document array ([red, red] ALL [red]) still match in both backends.

Co-authored-by: Cursor <cursoragent@cursor.com>
Consolidate the ALL/ONE integration tests into DocStoreQueryV1Test as a
nested ArrayMatchAllOneOperatorTest class, per review feedback:
- Nested JSONB array path (props.colors) covered on both MongoDB and
  Postgres via the shared document collection
- Native array columns (tags TEXT[], flags BOOLEAN[]) covered on the flat
  collection with both typed (compile-time DataType) and untyped
  (value-inference fallback) ArrayIdentifierExpression variants
- JSONB array column (props.colors) covered on the flat collection
- Duplicate-containing arrays ([red, red] ALL [red] -> true) covered via a
  dedicated collection, documenting set-containment semantics on real DBs

Co-authored-by: Cursor <cursoragent@cursor.com>
@lrathod

lrathod commented Sep 1, 2026

Copy link
Copy Markdown
Author

Addressed the review feedback in the latest commits:

Nested JSON arrays — ALL/ONE already go through the same array-source resolution infra as ANY (PostgresFieldIdentifierExpressionVisitor / MongoDollarPrefixingIdempotentParser), so nested paths like props.colors and scope.environmentScope.environmentIds work. Added coverage to prove it: unit tests in PostgresQueryParserTest (nested ALL + ONE) and MongoArrayFilterParserTest (nested ALL + ONE), plus integration tests in DocStoreQueryV1Test running nested-path ALL/ONE against both MongoDB and Postgres.

Integration tests — moved to DocStoreQueryV1Test (nested ArrayMatchAllOneOperatorTest), covering: nested JSONB array field (both stores), native array columns on the flat collection with typed and untyped ArrayIdentifierExpression (via PostgresArrayTypeProvider), and the JSONB array column on the flat collection.

Duplicates question[red, red] ALL [red] returns true in both backends. `` treats both operands as sets (duplicates collapsed), and Postgres @> is element-wise containment (each RHS element must exist in LHS; duplicates on either side are irrelevant). So ALL follows set semantics — order and duplicates don't matter. Added an integration test (`testAllWithDuplicatesInDocumentArray`) that asserts a document with `tags: [red, red]` matches `ALL [red]` on both datastores. (For completeness: ONE is unaffected by this — `[red, red]` has length 2, so it never matches ONE.)

lrathod and others added 3 commits September 1, 2026 11:39
Negative coverage:
- ALL/ONE reject a non-constant RHS with UnsupportedOperationException
  in both Mongo and Postgres parsers
- Empty value lists are rejected at construction by ConstantExpression
- Integration: non-array JSONB values do not match and do not error on
  Postgres, exercising the jsonb_typeof guard

Semantics documentation via integration tests on both datastores:
- ALL is order-independent: [red, blue] ALL [blue, red] matches
- ALL/ONE on a three-level nested array field (props.metadata.colors),
  including docs with missing intermediate objects

Co-authored-by: Cursor <cursoragent@cursor.com>
…ONE semantics

- Native Postgres arrays: drop COALESCE - NULL arrays are excluded by WHERE
  semantics anyway, and the unwrapped column reference keeps the filter
  GIN-indexable (SARGable)
- JSONB ONE: replace the per-value OR chain with a single <@ containment
  against the full filter list (with exactly one element, membership and
  containment are equivalent) - one bound param instead of N
- Mongo: guard ALL/ONE with $cond/$isArray so documents holding a non-array
  scalar no longer error out ($setIsSubset/$size reject non-array operands),
  matching the Postgres jsonb_typeof behavior; subsumes $ifNull
- Document that ONE counts raw elements, not distinct values
  ([red, red] ONE [red] is false), with an integration test on both stores;
  non-array scalar test now runs on Mongo too

Co-authored-by: Cursor <cursoragent@cursor.com>
Aligns with the service-level MATCH_EXACTLY_ONE name and reads
unambiguously ("exactly one element, in the given set"). Also notes a
future EXACTLY (set-equality) operator in the enum javadoc.

Co-authored-by: Cursor <cursoragent@cursor.com>
@lrathod lrathod changed the title Add ALL and ONE array filter operators for MongoDB and Postgres Add ALL and EXACTLY_ONE array filter operators for MongoDB and Postgres Sep 1, 2026
@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.45455% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.16%. Comparing base (330cbc2) to head (c884914).

Files with missing lines Patch % Lines
...1/vistors/PostgresFilterTypeExpressionVisitor.java 82.89% 7 Missing and 6 partials ⚠️
...ore/mongo/query/parser/MongoArrayFilterParser.java 90.62% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main     #323      +/-   ##
============================================
+ Coverage     81.06%   81.16%   +0.10%     
- Complexity     1617     1679      +62     
============================================
  Files           243      243              
  Lines          7656     7763     +107     
  Branches        755      769      +14     
============================================
+ Hits           6206     6301      +95     
- Misses          960      967       +7     
- Partials        490      495       +5     
Flag Coverage Δ
integration 81.16% <85.45%> (+0.10%) ⬆️
unit 58.50% <85.45%> (+1.35%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

.build())
.build();
// Only id 3 has exactly one element, and it is Black
assertEquals(1, collection.count(oneBlackOrWhite));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Would be better to assert on the actual docIds for 100% confidence.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. All ALL/EXACTLY_ONE integration tests in ArrayMatchAllOneOperatorTest now assert exact docId sets via collectNormalizedIds (e.g. assertEquals(Set.of("1", "2", "3", "7"), ...)), with fixture comments noting which ids match and why. This also implicitly pins the NULL/empty-array exclusions, and there is now an explicit testAllAndOneExcludeNullAndEmptyArrays for that guarantee.

}

/**
* A non-array value (here props.brand, a string) must simply not match instead of failing the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this how we're handling other operators too? Can you validate what happens when the LHS type does not conform to the RHS for an existing operator?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We validated ALL/EXACTLY_ONE type mismatch against existing EQ and IN in this suite (string vs number).

  • Mongo EQ and IN: 0 rows, no throw.
  • Postgres EQ of "10" on numeric quantity: matches (JSON ->> is text, so "10" equals stored 10).
  • Postgres IN of ["10"] on the same field: 0 rows (jsonb containment is typed).

ALL/EXACTLY_ONE with numeric RHS against string array props.colors: 0 rows on both Mongo and Postgres — same family as Mongo EQ/IN and Postgres IN, not Postgres EQ's text coercion. No new error path; mismatch is empty result for this JSON/document case.

*/
@ParameterizedTest
@ArgumentsSource(AllProvider.class)
void testAllAndOneOnNonArrayValueDoNotMatch(String dataStoreName) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, what happens in this case for top-level non-array fields?

@lrathod lrathod Sep 2, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for Postgres flat native column (item as text) scalar type non-array throws we say "operator does not exist: text @> text[]"

Document JSONB (mongo + postgres item) -> “top-level non-array → no match,”

.operator(ArrayOperator.ALL)
.filter(
RelationalExpression.of(
IdentifierExpression.of("props.metadata.colors"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we not use JsonIdentifierExpression here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JsonIdentifierExpression on nested document collections — NestedPostgresColTransformer rejects it. Nested ITs still use IdentifierExpression.of("props.colors"). Flat JSONB does use JsonIdentifierExpression.of("props", "colors"). Honest reply: we used it where the transformer supports it; document nested path still uses dotted identifiers.

.filter(
RelationalExpression.of(
IdentifierExpression.of("props.colors"),
IN,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does IN mean here semantically?

@lrathod lrathod Sep 2, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IN is element membership, not a second array-IN.

ALL + IN [a,b]: every RHS value is in the stored array (extras on the array OK).
EXACTLY_ONE + IN [a,b]: stored array length is 1, and that element is in {a,b}.
So EXACTLY_ONE ["red","green"] = singleton red or singleton green, not the two-element array ["red","green"].

Added test and also add the same in PR desc as well

}

@Test
void testAllOperatorWithJsonbArrayField() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is the legacy PG flow, with no support for first-class columns. That's why you'll see everything is accessed via -> accessor (for ex: jsonb_typeof(document->'tags')). What we need is the flat collection flow. You can parse queries for flat collections using:

    PostgresQueryParser postgresQueryParser =
        new PostgresQueryParser(TEST_TABLE, PostgresQueryTransformer.transform(query), new FlatPostgresFieldTransformer());

Can you please add test cases for that? I am actually surprised that I am unable to find a test class that tests parsed queries for flat collections. If that is indeed the case, would you mind creating one? Thanks :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But do keep these tests

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added flat-collection parser tests using PostgresQueryTransformer.transform(query) + FlatPostgresFieldTransformer, and kept the legacy-flow tests as you asked. Flat coverage now includes: native array columns (testAllOperatorWithNativeArrayField / testOneOperatorWithNativeArrayField, BOOLEAN[] variants, "tags" @> ? / array_length("tags", 1) = 1 AND "tags" && ?), flat JSONB columns via JsonIdentifierExpression (single- and multi-level paths), and compile-time field type vs value-based inference (testAllOperatorPrefersCompileTimeFieldTypeOverValueInference / fallback test).

[true,false] matches ids 5 and 8 (each a one-element flags array), not a parser bug; add [true]→1 to pin the singleton-true case.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ParameterizedTest
@ArgumentsSource(AllProvider.class)
void testAllAndOneOnNonArrayValueDoNotMatch(String dataStoreName) {
Collection collection = getCollection(dataStoreName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let us run these tests on flat collections please: Collection flatCollection = getFlatCollection(dataStoreName);.

Collection collection = getCollection(dataStoreName);: This returns the legacy storage mode PG in and therefore it's using jsonb_typeof guard.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added testAllAndOneOnNonArrayValueDoNotMatchFlat — same ALL/EXACTLY_ONE no-match assertion on the flat collection via JsonIdentifierExpression.of("props", "brand") (flat JSONB scalar, exercises the flat path instead of the legacy jsonb_typeof document guard).

*/
@ParameterizedTest
@ArgumentsSource(AllProvider.class)
void testAllIsOrderIndependent(String dataStoreName) throws IOException {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also validate this for flat?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added testAllIsOrderIndependentFlat — same fixture as array_match_test.json loaded into a temp flat table with a native tags TEXT[] column, run with both WITH_TYPE and WITHOUT_TYPE (PostgresArrayTypeProvider). ALL ["blue","red"] matches ids {1,2} on the flat native-array path too.

*/
@ParameterizedTest
@ArgumentsSource(AllProvider.class)
void testAllWithDuplicatesInDocumentArray(String dataStoreName) throws IOException {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, validate for flat?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added testAllWithDuplicatesInDocumentArrayFlat (temp flat table, native tags TEXT[], WITH_TYPE/WITHOUT_TYPE). ALL ["red"] matches ids {1,2,3,7} — {red,red} matches ALL ["red"] on the flat path as well (@> is element-wise containment).

*/
@ParameterizedTest
@ArgumentsSource(AllProvider.class)
void testOneWithDuplicatesInDocumentArray(String dataStoreName) throws IOException {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same ^

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added testOneWithDuplicatesInDocumentArrayFlat (temp flat table, native tags TEXT[], WITH_TYPE/WITHOUT_TYPE). EXACTLY_ONE ["red"] matches only id 3 — {red,red} is excluded because array_length = 2, same as the document path.

@suddendust

Copy link
Copy Markdown
Contributor

Can you add a test for parsed queries for flat collections? We might need that information for perf tuning.

lrathod and others added 2 commits September 4, 2026 11:17
…fy IN semantics

- Flat JSONB scalar no-match test (jsonb_typeof guard on the flat path)
- Flat native TEXT[] variants of order-independence and duplicate-element
  tests (WITH_TYPE/WITHOUT_TYPE), mirroring array_match_test.json
- Javadoc: inner IN is element membership; ALL = subset, EXACTLY_ONE =
  singleton whose element is in the RHS set
- Flat-collection postgres parser unit tests

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@lrathod

lrathod commented Sep 4, 2026

Copy link
Copy Markdown
Author

Yes — parsed-query tests for flat collections are in PostgresQueryParserTest (all use FlatPostgresFieldTransformer). The final SQL shapes:

Flat, native array column (testAllOperatorWithNativeArrayField, testOneOperatorWithNativeArrayField, + BOOLEAN[] / long[] variants):

-- ALL
SELECT * FROM "testCollection" WHERE "tags" @> ?                      -- param: text[]
-- EXACTLY_ONE
SELECT * FROM "testCollection" WHERE array_length("tags", 1) = 1 AND "tags" && ?

Flat, JSONB column (testAllOperatorWithJsonIdentifierOnFlatJsonbColumn, testOneOperatorWithJsonIdentifierOnFlatJsonbColumn, + multi-level path variants):

-- ALL
... WHERE (CASE WHEN jsonb_typeof("props"->'colors') = 'array'
           THEN "props"->'colors' ELSE '[]'::jsonb END) @> ?::jsonb
-- EXACTLY_ONE
... WHERE jsonb_array_length((CASE ... END)) = 1 AND (CASE ... END) <@ ?::jsonb

Index notes:

  • Native arrays: both @> and && are GIN-indexable (USING GIN (tags)); array_length(...) = 1 is a cheap residual.
  • Flat JSONB: the CASE WHEN jsonb_typeof(...) guard wraps the expression, so a plain GIN index on the "props" column will NOT be used — it would need an expression index on the full CASE expression, or we drop/avoid the guard where the schema guarantees arrays.

Also covered: compile-time element type (from ArrayIdentifierExpression) preferred over value-based inference, non-constant RHS rejected, empty RHS rejected at construction.

new FlatPostgresFieldTransformer());

String sql = postgresQueryParser.parse();
assertEquals(

@suddendust suddendust Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WHERE jsonb_typeof("props"->'colors') = 'array'
  AND jsonb_array_length("props"->'colors') = 1
  AND "props"->'colors' <@ ?::jsonb

Is this better?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — containment (@> and <@) is total on non-array jsonb: it returns false on scalars and NULL on missing keys, never errors. So we've dropped the CASE guard for ALL entirely (WHERE "props"->'colors' @> ?::jsonb) and for the EXACTLY_ONE <@ conjunct. The one place a guard is still required is jsonb_array_length — it raises cannot get array length of a scalar on non-arrays, and since Postgres doesn't guarantee WHERE conjunct evaluation order, an AND-chained jsonb_typeof check isn't a safe guard; the length check keeps a CASE: (CASE WHEN jsonb_typeof(...) = 'array' THEN jsonb_array_length(...) ELSE 0 END) = 1 AND "props"->'colors' <@ ?::jsonb. Unit + integration tests updated.

lrathod and others added 3 commits September 4, 2026 13:21
Document (mongo + postgres) and flat native-array variants asserting that
rows with missing/NULL or empty array fields never match, with positive
controls so the tests are not vacuous.

Co-authored-by: Cursor <cursoragent@cursor.com>
…_ONE length check

jsonb @>/<@ are total on non-array scalars (false/NULL, never error), so the
CASE guard was unnecessary for ALL and for the EXACTLY_ONE containment
conjunct. jsonb_array_length raises on non-arrays and Postgres does not
guarantee WHERE conjunct evaluation order, so the length check keeps its
CASE guard.

Co-authored-by: Cursor <cursoragent@cursor.com>
…top-level semantics

Co-authored-by: Cursor <cursoragent@cursor.com>

final String guardedLength =
String.format(
"(CASE WHEN jsonb_typeof(%s) = 'array' THEN jsonb_array_length(%s) ELSE 0 END)",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need case @lrathod ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only for EXACTLY_ONE length check on JSONB fields

jsonb_array_length() raises cannot get array length of a scalar on non-arrays. And we can't just AND-chain jsonb_typeof(x) = 'array' AND jsonb_array_length(x) = 1 because Postgres doesn't guarantee WHERE conjunct evaluation order — the planner may evaluate the length call first. CASE is the only order-forcing construct → kept, but only around the length check:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants